/*******************************************************************************
 * Copyright 2011, 2012 Chris Banes.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package com.core.widget.pulltorefresh;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import com.core.widget.pulltorefresh.internal.FlipLoadingLayout;
import com.core.widget.pulltorefresh.internal.LoadingLayout;
import com.core.widget.pulltorefresh.internal.RotateLoadingLayout;
import com.core.widget.pulltorefresh.internal.Utils;
import com.core.widget.pulltorefresh.internal.ViewCompat;
import com.cptravel.R;

public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T>
{
    // ===========================================================
    // Constants
    // ===========================================================
    static final boolean DEBUG = true;
    static final boolean USE_HW_LAYERS = false;
    static final String LOG_TAG = "PullToRefresh";
    static final float FRICTION = 2.0f;
    public static final int SMOOTH_SCROLL_DURATION_MS = 200;
    public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325;
    static final int DEMO_SCROLL_INTERVAL = 225;
    static final String STATE_STATE = "ptr_state";
    static final String STATE_MODE = "ptr_mode";
    static final String STATE_CURRENT_MODE = "ptr_current_mode";
    static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling";
    static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view";
    static final String STATE_SUPER = "ptr_super";
    // ===========================================================
    // Fields
    // ===========================================================
    private int mTouchSlop;
    private float mLastMotionX, mLastMotionY;
    private float mInitialMotionX, mInitialMotionY;
    private boolean mIsBeingDragged = false;
    private State mState = State.RESET;
    private Mode mMode = Mode.getDefault();
    private Mode mCurrentMode;
    T mRefreshableView;
    private FrameLayout mRefreshableViewWrapper;
    private boolean mShowViewWhileRefreshing = true;
    private boolean mScrollingWhileRefreshingEnabled = false;
    private boolean mFilterTouchEvents = true;
    private boolean mOverScrollEnabled = true;
    private boolean mLayoutVisibilityChangesEnabled = true;
    private Interpolator mScrollAnimationInterpolator;
    private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();
    private LoadingLayout mHeaderLayout;
    private LoadingLayout mFooterLayout;
    private OnRefreshListener<T> mOnRefreshListener;
    private OnRefreshListener2<T> mOnRefreshListener2;
    private OnPullEventListener<T> mOnPullEventListener;
    private SmoothScrollRunnable mCurrentSmoothScrollRunnable;

    // ===========================================================
    // Constructors
    // ===========================================================
    public PullToRefreshBase(Context context)
    {
        super(context);
        init(context, null);
    }

    public PullToRefreshBase(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        init(context, attrs);
    }

    public PullToRefreshBase(Context context, Mode mode)
    {
        super(context);
        mMode = mode;
        init(context, null);
    }

    public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle)
    {
        super(context);
        mMode = mode;
        mLoadingAnimationStyle = animStyle;
        init(context, null);
    }

    @Override
    public void addView(View child , int index , ViewGroup.LayoutParams params)
    {
        if (DEBUG)
        {
            Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName());
        }
        final T refreshableView = getRefreshableView();
        if (refreshableView instanceof ViewGroup)
        {
            ((ViewGroup) refreshableView).addView(child, index, params);
        }
        else
        {
            throw new UnsupportedOperationException("Refreshable View is not a ViewGroup so can't addView");
        }
    }

    @Override
    public final boolean demo()
    {
        if (mMode.showHeaderLoadingLayout() && isReadyForPullStart())
        {
            smoothScrollToAndBack(-getHeaderSize() * 2);
            return true;
        }
        else if (mMode.showFooterLoadingLayout() && isReadyForPullEnd())
        {
            smoothScrollToAndBack(getFooterSize() * 2);
            return true;
        }
        return false;
    }

    @Override
    public final Mode getCurrentMode()
    {
        return mCurrentMode;
    }

    @Override
    public final boolean getFilterTouchEvents()
    {
        return mFilterTouchEvents;
    }

    @Override
    public final ILoadingLayout getLoadingLayoutProxy()
    {
        return getLoadingLayoutProxy(true, true);
    }

    @Override
    public final ILoadingLayout getLoadingLayoutProxy(boolean includeStart , boolean includeEnd)
    {
        return createLoadingLayoutProxy(includeStart, includeEnd);
    }

    @Override
    public final Mode getMode()
    {
        return mMode;
    }

    @Override
    public final T getRefreshableView()
    {
        return mRefreshableView;
    }

    @Override
    public final boolean getShowViewWhileRefreshing()
    {
        return mShowViewWhileRefreshing;
    }

    @Override
    public final State getState()
    {
        return mState;
    }

    /**
     * @deprecated See {@link #isScrollingWhileRefreshingEnabled()}.
     */
    @Deprecated
    public final boolean isDisableScrollingWhileRefreshing()
    {
        return !isScrollingWhileRefreshingEnabled();
    }

    @Override
    public final boolean isPullToRefreshEnabled()
    {
        return mMode.permitsPullToRefresh();
    }

    @Override
    public final boolean isPullToRefreshOverScrollEnabled()
    {
        return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD && mOverScrollEnabled && OverscrollHelper.isAndroidOverScrollEnabled(mRefreshableView);
    }

    @Override
    public final boolean isRefreshing()
    {
        return mState == State.REFRESHING || mState == State.MANUAL_REFRESHING;
    }

    @Override
    public final boolean isScrollingWhileRefreshingEnabled()
    {
        return mScrollingWhileRefreshingEnabled;
    }

    @Override
    public final boolean onInterceptTouchEvent(MotionEvent event)
    {
        if (!isPullToRefreshEnabled())
        {
            return false;
        }
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP)
        {
            mIsBeingDragged = false;
            return false;
        }
        if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged)
        {
            return true;
        }
        switch (action)
        {
            case MotionEvent.ACTION_MOVE:
            {
                // If we're refreshing, and the flag is set. Eat all MOVE events
                if (!mScrollingWhileRefreshingEnabled && isRefreshing())
                {
                    return true;
                }
                if (isReadyForPull())
                {
                    final float y = event.getY(), x = event.getX();
                    final float diff, oppositeDiff, absDiff;
                    // We need to use the correct values, based on scroll
                    // direction
                    switch (getPullToRefreshScrollDirection())
                    {
                        case HORIZONTAL:
                            diff = x - mLastMotionX;
                            oppositeDiff = y - mLastMotionY;
                            break;
                        case VERTICAL:
                        default:
                            diff = y - mLastMotionY;
                            oppositeDiff = x - mLastMotionX;
                            break;
                    }
                    absDiff = Math.abs(diff);
                    if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff)))
                    {
                        if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart())
                        {
                            mLastMotionY = y;
                            mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH)
                            {
                                mCurrentMode = Mode.PULL_FROM_START;
                            }
                        }
                        else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd())
                        {
                            mLastMotionY = y;
                            mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH)
                            {
                                mCurrentMode = Mode.PULL_FROM_END;
                            }
                        }
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                if (isReadyForPull())
                {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    mIsBeingDragged = false;
                }
                break;
            }
        }
        return mIsBeingDragged;
    }

    @Override
    public final void onRefreshComplete()
    {
        if (isRefreshing())
        {
            setState(State.RESET);
        }
    }

    @Override
    public final boolean onTouchEvent(MotionEvent event)
    {
        if (!isPullToRefreshEnabled())
        {
            return false;
        }
        // If we're refreshing, and the flag is set. Eat the event
        if (!mScrollingWhileRefreshingEnabled && isRefreshing())
        {
            return true;
        }
        if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0)
        {
            return false;
        }
        switch (event.getAction())
        {
            case MotionEvent.ACTION_MOVE:
            {
                if (mIsBeingDragged)
                {
                    mLastMotionY = event.getY();
                    mLastMotionX = event.getX();
                    pullEvent();
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                if (isReadyForPull())
                {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
            {
                if (mIsBeingDragged)
                {
                    mIsBeingDragged = false;
                    if (mState == State.RELEASE_TO_REFRESH && (null != mOnRefreshListener || null != mOnRefreshListener2))
                    {
                        setState(State.REFRESHING, true);
                        return true;
                    }
                    // If we're already refreshing, just scroll back to the top
                    if (isRefreshing())
                    {
                        smoothScrollTo(0);
                        return true;
                    }
                    // If we haven't returned by here, then we're not in a state
                    // to pull, so just reset
                    setState(State.RESET);
                    return true;
                }
                break;
            }
        }
        return false;
    }

    @Override
    public final void setScrollingWhileRefreshingEnabled(boolean allowScrollingWhileRefreshing)
    {
        mScrollingWhileRefreshingEnabled = allowScrollingWhileRefreshing;
    }

    /**
     * @deprecated See {@link #setScrollingWhileRefreshingEnabled(boolean)}
     */
    @Deprecated
    public void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing)
    {
        setScrollingWhileRefreshingEnabled(!disableScrollingWhileRefreshing);
    }

    @Override
    public final void setFilterTouchEvents(boolean filterEvents)
    {
        mFilterTouchEvents = filterEvents;
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    @Deprecated
    public void setLastUpdatedLabel(CharSequence label)
    {
        getLoadingLayoutProxy().setLastUpdatedLabel(label);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    @Deprecated
    public void setLoadingDrawable(Drawable drawable)
    {
        getLoadingLayoutProxy().setLoadingDrawable(drawable);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    @Deprecated
    public void setLoadingDrawable(Drawable drawable , Mode mode)
    {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setLoadingDrawable(drawable);
    }

    @Override
    public void setLongClickable(boolean longClickable)
    {
        getRefreshableView().setLongClickable(longClickable);
    }

    @Override
    public final void setMode(Mode mode)
    {
        if (mode != mMode)
        {
            if (DEBUG)
            {
                Log.d(LOG_TAG, "Setting mode to: " + mode);
            }
            mMode = mode;
            updateUIForMode();
        }
    }

    @Override
    public void setOnPullEventListener(OnPullEventListener<T> listener)
    {
        mOnPullEventListener = listener;
    }

    @Override
    public final void setOnRefreshListener(OnRefreshListener<T> listener)
    {
        mOnRefreshListener = listener;
        mOnRefreshListener2 = null;
    }

    @Override
    public final void setOnRefreshListener(OnRefreshListener2<T> listener)
    {
        mOnRefreshListener2 = listener;
        mOnRefreshListener = null;
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    @Deprecated
    public void setPullLabel(CharSequence pullLabel)
    {
        getLoadingLayoutProxy().setPullLabel(pullLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    @Deprecated
    public void setPullLabel(CharSequence pullLabel , Mode mode)
    {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setPullLabel(pullLabel);
    }

    /**
     * @param enable Whether Pull-To-Refresh should be used
     * @deprecated This simple calls setMode with an appropriate mode based on
     *             the passed value.
     */
    @Deprecated
    public final void setPullToRefreshEnabled(boolean enable)
    {
        setMode(enable ? Mode.getDefault() : Mode.DISABLED);
    }

    @Override
    public final void setPullToRefreshOverScrollEnabled(boolean enabled)
    {
        mOverScrollEnabled = enabled;
    }

    @Override
    public final void setRefreshing()
    {
        setRefreshing(true);
    }

    @Override
    public final void setRefreshing(boolean doScroll)
    {
        if (!isRefreshing())
        {
            setState(State.MANUAL_REFRESHING, doScroll);
        }
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    @Deprecated
    public void setRefreshingLabel(CharSequence refreshingLabel)
    {
        getLoadingLayoutProxy().setRefreshingLabel(refreshingLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    @Deprecated
    public void setRefreshingLabel(CharSequence refreshingLabel , Mode mode)
    {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setRefreshingLabel(refreshingLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    @Deprecated
    public void setReleaseLabel(CharSequence releaseLabel)
    {
        setReleaseLabel(releaseLabel, Mode.BOTH);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    @Deprecated
    public void setReleaseLabel(CharSequence releaseLabel , Mode mode)
    {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setReleaseLabel(releaseLabel);
    }

    @Override
    public void setScrollAnimationInterpolator(Interpolator interpolator)
    {
        mScrollAnimationInterpolator = interpolator;
    }

    @Override
    public final void setShowViewWhileRefreshing(boolean showView)
    {
        mShowViewWhileRefreshing = showView;
    }

    /**
     * @return Either {@link Orientation#VERTICAL} or
     *         {@link Orientation#HORIZONTAL} depending on the scroll direction.
     */
    public abstract Orientation getPullToRefreshScrollDirection();

    final void setState(State state , final boolean... params)
    {
        mState = state;
        if (DEBUG)
        {
            Log.d(LOG_TAG, "State: " + mState.name());
        }
        switch (mState)
        {
            case RESET:
                onReset();
                break;
            case PULL_TO_REFRESH:
                onPullToRefresh();
                break;
            case RELEASE_TO_REFRESH:
                onReleaseToRefresh();
                break;
            case REFRESHING:
            case MANUAL_REFRESHING:
                onRefreshing(params[0]);
                break;
            case OVERSCROLLING:
                // NO-OP
                break;
        }
        // Call OnPullEventListener
        if (null != mOnPullEventListener)
        {
            mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
        }
    }

    /**
     * Used internally for adding view. Need because we override addView to
     * pass-through to the Refreshable View
     */
    protected final void addViewInternal(View child , int index , ViewGroup.LayoutParams params)
    {
        super.addView(child, index, params);
    }

    /**
     * Used internally for adding view. Need because we override addView to
     * pass-through to the Refreshable View
     */
    protected final void addViewInternal(View child , ViewGroup.LayoutParams params)
    {
        super.addView(child, -1, params);
    }

    protected LoadingLayout createLoadingLayout(Context context , Mode mode , TypedArray attrs)
    {
        LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode, getPullToRefreshScrollDirection(), attrs);
        layout.setVisibility(View.INVISIBLE);
        return layout;
    }

    /**
     * Used internally for {@link #getLoadingLayoutProxy(boolean, boolean)}.
     * Allows derivative classes to include any extra LoadingLayouts.
     */
    protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart , final boolean includeEnd)
    {
        LoadingLayoutProxy proxy = new LoadingLayoutProxy();
        if (includeStart && mMode.showHeaderLoadingLayout())
        {
            proxy.addLayout(mHeaderLayout);
        }
        if (includeEnd && mMode.showFooterLoadingLayout())
        {
            proxy.addLayout(mFooterLayout);
        }
        return proxy;
    }

    /**
     * This is implemented by derived classes to return the created View. If you
     * need to use a custom View (such as a custom ListView), override this
     * method and return an instance of your custom class.
     * <p/>
     * Be sure to set the ID of the view in this method, especially if you're
     * using a ListActivity or ListFragment.
     * 
     * @param context Context to create view with
     * @param attrs AttributeSet from wrapped class. Means that anything you
     *            include in the XML layout declaration will be routed to the
     *            created View
     * @return New instance of the Refreshable View
     */
    protected abstract T createRefreshableView(Context context , AttributeSet attrs);

    protected final void disableLoadingLayoutVisibilityChanges()
    {
        mLayoutVisibilityChangesEnabled = false;
    }

    protected final LoadingLayout getFooterLayout()
    {
        return mFooterLayout;
    }

    protected final int getFooterSize()
    {
        return mFooterLayout.getContentSize();
    }

    protected final LoadingLayout getHeaderLayout()
    {
        return mHeaderLayout;
    }

    protected final int getHeaderSize()
    {
        return mHeaderLayout.getContentSize();
    }

    protected int getPullToRefreshScrollDuration()
    {
        return SMOOTH_SCROLL_DURATION_MS;
    }

    protected int getPullToRefreshScrollDurationLonger()
    {
        return SMOOTH_SCROLL_LONG_DURATION_MS;
    }

    protected FrameLayout getRefreshableViewWrapper()
    {
        return mRefreshableViewWrapper;
    }

    /**
     * Allows Derivative classes to handle the XML Attrs without creating a
     * TypedArray themsevles
     * 
     * @param a - TypedArray of PullToRefresh Attributes
     */
    protected void handleStyledAttributes(TypedArray a)
    {
    }

    /**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling from the end.
     * 
     * @return true if the View is currently in the correct state (for example,
     *         bottom of a ListView)
     */
    protected abstract boolean isReadyForPullEnd();

    /**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling from the start.
     * 
     * @return true if the View is currently the correct state (for example, top
     *         of a ListView)
     */
    protected abstract boolean isReadyForPullStart();

    /**
     * Called by {@link #onRestoreInstanceState(Parcelable)} so that derivative
     * classes can handle their saved instance state.
     * 
     * @param savedInstanceState - Bundle which contains saved instance state.
     */
    protected void onPtrRestoreInstanceState(Bundle savedInstanceState)
    {
    }

    /**
     * Called by {@link #onSaveInstanceState()} so that derivative classes can
     * save their instance state.
     * 
     * @param saveState - Bundle to be updated with saved state.
     */
    protected void onPtrSaveInstanceState(Bundle saveState)
    {
    }

    /**
     * Called when the UI has been to be updated to be in the
     * {@link State#PULL_TO_REFRESH} state.
     */
    protected void onPullToRefresh()
    {
        switch (mCurrentMode)
        {
            case PULL_FROM_END:
                mFooterLayout.pullToRefresh();
                break;
            case PULL_FROM_START:
                mHeaderLayout.pullToRefresh();
                break;
            default:
                // NO-OP
                break;
        }
    }

    /**
     * Called when the UI has been to be updated to be in the
     * {@link State#REFRESHING} or {@link State#MANUAL_REFRESHING} state.
     * 
     * @param doScroll - Whether the UI should scroll for this event.
     */
    protected void onRefreshing(final boolean doScroll)
    {
        if (mMode.showHeaderLoadingLayout())
        {
            mHeaderLayout.refreshing();
        }
        if (mMode.showFooterLoadingLayout())
        {
            mFooterLayout.refreshing();
        }
        if (doScroll)
        {
            if (mShowViewWhileRefreshing)
            {
                // Call Refresh Listener when the Scroll has finished
                OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener()
                {
                    @Override
                    public void onSmoothScrollFinished()
                    {
                        callRefreshListener();
                    }
                };
                switch (mCurrentMode)
                {
                    case MANUAL_REFRESH_ONLY:
                    case PULL_FROM_END:
                        smoothScrollTo(getFooterSize(), listener);
                        break;
                    default:
                    case PULL_FROM_START:
                        smoothScrollTo(-getHeaderSize(), listener);
                        break;
                }
            }
            else
            {
                smoothScrollTo(0);
            }
        }
        else
        {
            // We're not scrolling, so just call Refresh Listener now
            callRefreshListener();
        }
    }

    /**
     * Called when the UI has been to be updated to be in the
     * {@link State#RELEASE_TO_REFRESH} state.
     */
    protected void onReleaseToRefresh()
    {
        switch (mCurrentMode)
        {
            case PULL_FROM_END:
                mFooterLayout.releaseToRefresh();
                break;
            case PULL_FROM_START:
                mHeaderLayout.releaseToRefresh();
                break;
            default:
                // NO-OP
                break;
        }
    }

    /**
     * Called when the UI has been to be updated to be in the
     * {@link State#RESET} state.
     */
    protected void onReset()
    {
        mIsBeingDragged = false;
        mLayoutVisibilityChangesEnabled = true;
        // Always reset both layouts, just in case...
        mHeaderLayout.reset();
        mFooterLayout.reset();
        smoothScrollTo(0);
    }

    @Override
    protected final void onRestoreInstanceState(Parcelable state)
    {
        if (state instanceof Bundle)
        {
            Bundle bundle = (Bundle) state;
            setMode(Mode.mapIntToValue(bundle.getInt(STATE_MODE, 0)));
            mCurrentMode = Mode.mapIntToValue(bundle.getInt(STATE_CURRENT_MODE, 0));
            mScrollingWhileRefreshingEnabled = bundle.getBoolean(STATE_SCROLLING_REFRESHING_ENABLED, false);
            mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true);
            // Let super Restore Itself
            super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
            State viewState = State.mapIntToValue(bundle.getInt(STATE_STATE, 0));
            if (viewState == State.REFRESHING || viewState == State.MANUAL_REFRESHING)
            {
                setState(viewState, true);
            }
            // Now let derivative classes restore their state
            onPtrRestoreInstanceState(bundle);
            return;
        }
        super.onRestoreInstanceState(state);
    }

    @Override
    protected final Parcelable onSaveInstanceState()
    {
        Bundle bundle = new Bundle();
        // Let derivative classes get a chance to save state first, that way we
        // can make sure they don't overrite any of our values
        onPtrSaveInstanceState(bundle);
        bundle.putInt(STATE_STATE, mState.getIntValue());
        bundle.putInt(STATE_MODE, mMode.getIntValue());
        bundle.putInt(STATE_CURRENT_MODE, mCurrentMode.getIntValue());
        bundle.putBoolean(STATE_SCROLLING_REFRESHING_ENABLED, mScrollingWhileRefreshingEnabled);
        bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing);
        bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
        return bundle;
    }

    @Override
    protected final void onSizeChanged(int w , int h , int oldw , int oldh)
    {
        if (DEBUG)
        {
            Log.d(LOG_TAG, String.format("onSizeChanged. W: %d, H: %d", w, h));
        }
        super.onSizeChanged(w, h, oldw, oldh);
        // We need to update the header/footer when our size changes
        refreshLoadingViewsSize();
        // Update the Refreshable View layout
        refreshRefreshableViewSize(w, h);
        /**
         * As we're currently in a Layout Pass, we need to schedule another one
         * to layout any changes we've made here
         */
        post(new Runnable()
        {
            @Override
            public void run()
            {
                requestLayout();
            }
        });
    }

    /**
     * Re-measure the Loading Views height, and adjust internal padding as
     * necessary
     */
    protected final void refreshLoadingViewsSize()
    {
        final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);
        int pLeft = getPaddingLeft();
        int pTop = getPaddingTop();
        int pRight = getPaddingRight();
        int pBottom = getPaddingBottom();
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                if (mMode.showHeaderLoadingLayout())
                {
                    mHeaderLayout.setWidth(maximumPullScroll);
                    pLeft = -maximumPullScroll;
                }
                else
                {
                    pLeft = 0;
                }
                if (mMode.showFooterLoadingLayout())
                {
                    mFooterLayout.setWidth(maximumPullScroll);
                    pRight = -maximumPullScroll;
                }
                else
                {
                    pRight = 0;
                }
                break;
            case VERTICAL:
                if (mMode.showHeaderLoadingLayout())
                {
                    mHeaderLayout.setHeight(maximumPullScroll);
                    pTop = -maximumPullScroll;
                }
                else
                {
                    pTop = 0;
                }
                if (mMode.showFooterLoadingLayout())
                {
                    mFooterLayout.setHeight(maximumPullScroll);
                    pBottom = -maximumPullScroll;
                }
                else
                {
                    pBottom = 0;
                }
                break;
        }
        if (DEBUG)
        {
            Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom));
        }
        setPadding(pLeft, pTop, pRight, pBottom);
    }

    protected final void refreshRefreshableViewSize(int width , int height)
    {
        // We need to set the Height of the Refreshable View to the same as
        // this layout
        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mRefreshableViewWrapper.getLayoutParams();
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                if (lp.width != width)
                {
                    lp.width = width;
                    mRefreshableViewWrapper.requestLayout();
                }
                break;
            case VERTICAL:
                if (lp.height != height)
                {
                    lp.height = height;
                    mRefreshableViewWrapper.requestLayout();
                }
                break;
        }
    }

    /**
     * Helper method which just calls scrollTo() in the correct scrolling
     * direction.
     * 
     * @param value - New Scroll value
     */
    protected final void setHeaderScroll(int value)
    {
        if (DEBUG)
        {
            Log.d(LOG_TAG, "setHeaderScroll: " + value);
        }
        // Clamp value to with pull scroll range
        final int maximumPullScroll = getMaximumPullScroll();
        value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));
        if (mLayoutVisibilityChangesEnabled)
        {
            if (value < 0)
            {
                mHeaderLayout.setVisibility(View.VISIBLE);
            }
            else if (value > 0)
            {
                mFooterLayout.setVisibility(View.VISIBLE);
            }
            else
            {
                mHeaderLayout.setVisibility(View.INVISIBLE);
                mFooterLayout.setVisibility(View.INVISIBLE);
            }
        }
        if (USE_HW_LAYERS)
        {
            /**
             * Use a Hardware Layer on the Refreshable View if we've scrolled at
             * all. We don't use them on the Header/Footer Views as they change
             * often, which would negate any HW layer performance boost.
             */
            ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE);
        }
        switch (getPullToRefreshScrollDirection())
        {
            case VERTICAL:
                scrollTo(0, value);
                break;
            case HORIZONTAL:
                scrollTo(value, 0);
                break;
        }
    }

    /**
     * Smooth Scroll to position using the default duration of
     * {@value #SMOOTH_SCROLL_DURATION_MS} ms.
     * 
     * @param scrollValue - Position to scroll to
     */
    protected final void smoothScrollTo(int scrollValue)
    {
        smoothScrollTo(scrollValue, getPullToRefreshScrollDuration());
    }

    /**
     * Smooth Scroll to position using the default duration of
     * {@value #SMOOTH_SCROLL_DURATION_MS} ms.
     * 
     * @param scrollValue - Position to scroll to
     * @param listener - Listener for scroll
     */
    protected final void smoothScrollTo(int scrollValue , OnSmoothScrollFinishedListener listener)
    {
        smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener);
    }

    /**
     * Smooth Scroll to position using the longer default duration of
     * {@value #SMOOTH_SCROLL_LONG_DURATION_MS} ms.
     * 
     * @param scrollValue - Position to scroll to
     */
    protected final void smoothScrollToLonger(int scrollValue)
    {
        smoothScrollTo(scrollValue, getPullToRefreshScrollDurationLonger());
    }

    /**
     * Updates the View State when the mode has been set. This does not do any
     * checking that the mode is different to current state so always updates.
     */
    protected void updateUIForMode()
    {
        // We need to use the correct LayoutParam values, based on scroll
        // direction
        final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams();
        // Remove Header, and then add Header Loading View again if needed
        if (this == mHeaderLayout.getParent())
        {
            removeView(mHeaderLayout);
        }
        if (mMode.showHeaderLoadingLayout())
        {
            addViewInternal(mHeaderLayout, 0, lp);
        }
        // Remove Footer, and then add Footer Loading View again if needed
        if (this == mFooterLayout.getParent())
        {
            removeView(mFooterLayout);
        }
        if (mMode.showFooterLoadingLayout())
        {
            addViewInternal(mFooterLayout, lp);
        }
        // Hide Loading Views
        refreshLoadingViewsSize();
        // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
        // set it to pull down
        mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
    }

    private void addRefreshableView(Context context , T refreshableView)
    {
        mRefreshableViewWrapper = new FrameLayout(context);
        mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addViewInternal(mRefreshableViewWrapper, new LinearLayout.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT));
    }

    private void callRefreshListener()
    {
        if (null != mOnRefreshListener)
        {
            mOnRefreshListener.onRefresh(this);
        }
        else if (null != mOnRefreshListener2)
        {
            if (mCurrentMode == Mode.PULL_FROM_START)
            {
                mOnRefreshListener2.onPullDownToRefresh(this);
            }
            else if (mCurrentMode == Mode.PULL_FROM_END)
            {
                mOnRefreshListener2.onPullUpToRefresh(this);
            }
        }
    }

    @SuppressWarnings("deprecation")
    private void init(Context context , AttributeSet attrs)
    {
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                setOrientation(LinearLayout.HORIZONTAL);
                break;
            case VERTICAL:
            default:
                setOrientation(LinearLayout.VERTICAL);
                break;
        }
        setGravity(Gravity.CENTER);
        ViewConfiguration config = ViewConfiguration.get(context);
        mTouchSlop = config.getScaledTouchSlop();
        // Styleables from XML
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
        if (a.hasValue(R.styleable.PullToRefresh_ptrMode))
        {
            mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
        }
        if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle))
        {
            mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrAnimationStyle, 0));
        }
        // Refreshable View
        // By passing the attrs, we can add ListView/GridView params via XML
        mRefreshableView = createRefreshableView(context, attrs);
        addRefreshableView(context, mRefreshableView);
        // We need to create now layouts now
        mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
        mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);
        /**
         * Styleables from XML
         */
        if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground))
        {
            Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
            if (null != background)
            {
                mRefreshableView.setBackgroundDrawable(background);
            }
        }
        else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground))
        {
            Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground");
            Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
            if (null != background)
            {
                mRefreshableView.setBackgroundDrawable(background);
            }
        }
        if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll))
        {
            mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
        }
        if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled))
        {
            mScrollingWhileRefreshingEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
        }
        // Let the derivative classes have a go at handling attributes, then
        // recycle them...
        handleStyledAttributes(a);
        a.recycle();
        // Finally update the UI for the modes
        updateUIForMode();
    }

    private boolean isReadyForPull()
    {
        switch (mMode)
        {
            case PULL_FROM_START:
                return isReadyForPullStart();
            case PULL_FROM_END:
                return isReadyForPullEnd();
            case BOTH:
                return isReadyForPullEnd() || isReadyForPullStart();
            default:
                return false;
        }
    }

    /**
     * Actions a Pull Event
     * 
     * @return true if the Event has been handled, false if there has been no
     *         change
     */
    private void pullEvent()
    {
        final int newScrollValue;
        final int itemDimension;
        final float initialMotionValue, lastMotionValue;
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                initialMotionValue = mInitialMotionX;
                lastMotionValue = mLastMotionX;
                break;
            case VERTICAL:
            default:
                initialMotionValue = mInitialMotionY;
                lastMotionValue = mLastMotionY;
                break;
        }
        switch (mCurrentMode)
        {
            case PULL_FROM_END:
                newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
                itemDimension = getFooterSize();
                break;
            case PULL_FROM_START:
            default:
                newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
                itemDimension = getHeaderSize();
                break;
        }
        setHeaderScroll(newScrollValue);
        if (newScrollValue != 0 && !isRefreshing())
        {
            float scale = Math.abs(newScrollValue) / (float) itemDimension;
            switch (mCurrentMode)
            {
                case PULL_FROM_END:
                    mFooterLayout.onPull(scale);
                    break;
                case PULL_FROM_START:
                default:
                    mHeaderLayout.onPull(scale);
                    break;
            }
            if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue))
            {
                setState(State.PULL_TO_REFRESH);
            }
            else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue))
            {
                setState(State.RELEASE_TO_REFRESH);
            }
        }
    }

    private LinearLayout.LayoutParams getLoadingLayoutLayoutParams()
    {
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                return new LinearLayout.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT);
            case VERTICAL:
            default:
                return new LinearLayout.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    }

    private int getMaximumPullScroll()
    {
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                return Math.round(getWidth() / FRICTION);
            case VERTICAL:
            default:
                return Math.round(getHeight() / FRICTION);
        }
    }

    /**
     * Smooth Scroll to position using the specific duration
     * 
     * @param scrollValue - Position to scroll to
     * @param duration - Duration of animation in milliseconds
     */
    private final void smoothScrollTo(int scrollValue , long duration)
    {
        smoothScrollTo(scrollValue, duration, 0, null);
    }

    private final void smoothScrollTo(int newScrollValue , long duration , long delayMillis , OnSmoothScrollFinishedListener listener)
    {
        if (null != mCurrentSmoothScrollRunnable)
        {
            mCurrentSmoothScrollRunnable.stop();
        }
        final int oldScrollValue;
        switch (getPullToRefreshScrollDirection())
        {
            case HORIZONTAL:
                oldScrollValue = getScrollX();
                break;
            case VERTICAL:
            default:
                oldScrollValue = getScrollY();
                break;
        }
        if (oldScrollValue != newScrollValue)
        {
            if (null == mScrollAnimationInterpolator)
            {
                // Default interpolator is a Decelerate Interpolator
                mScrollAnimationInterpolator = new DecelerateInterpolator();
            }
            mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);
            if (delayMillis > 0)
            {
                postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
            }
            else
            {
                post(mCurrentSmoothScrollRunnable);
            }
        }
    }

    private final void smoothScrollToAndBack(int y)
    {
        smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS, 0, new OnSmoothScrollFinishedListener()
        {
            @Override
            public void onSmoothScrollFinished()
            {
                smoothScrollTo(0, SMOOTH_SCROLL_DURATION_MS, DEMO_SCROLL_INTERVAL, null);
            }
        });
    }

    public static enum AnimationStyle {
        /**
         * This is the default for Android-PullToRefresh. Allows you to use any
         * drawable, which is automatically rotated and used as a Progress Bar.
         */
        ROTATE,
        /**
         * This is the old default, and what is commonly used on iOS. Uses an
         * arrow image which flips depending on where the user has scrolled.
         */
        FLIP;
        static AnimationStyle getDefault()
        {
            return ROTATE;
        }

        /**
         * Maps an int to a specific mode. This is needed when saving state, or
         * inflating the view from XML where the mode is given through a attr
         * int.
         * 
         * @param modeInt - int to map a Mode to
         * @return Mode that modeInt maps to, or ROTATE by default.
         */
        static AnimationStyle mapIntToValue(int modeInt)
        {
            switch (modeInt)
            {
                case 0x0:
                default:
                    return ROTATE;
                case 0x1:
                    return FLIP;
            }
        }

        LoadingLayout createLoadingLayout(Context context , Mode mode , Orientation scrollDirection , TypedArray attrs)
        {
            switch (this)
            {
                case ROTATE:
                default:
                    return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
                case FLIP:
                    return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
            }
        }
    }

    public static enum Mode {
        /**
         * Disable all Pull-to-Refresh gesture and Refreshing handling
         */
        DISABLED(0x0),
        /**
         * Only allow the user to Pull from the start of the Refreshable View to
         * refresh. The start is either the Top or Left, depending on the
         * scrolling direction.
         */
        PULL_FROM_START(0x1),
        /**
         * Only allow the user to Pull from the end of the Refreshable View to
         * refresh. The start is either the Bottom or Right, depending on the
         * scrolling direction.
         */
        PULL_FROM_END(0x2),
        /**
         * Allow the user to both Pull from the start, from the end to refresh.
         */
        BOTH(0x3),
        /**
         * Disables Pull-to-Refresh gesture handling, but allows manually
         * setting the Refresh state via
         * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
         */
        MANUAL_REFRESH_ONLY(0x4);
        /**
         * @deprecated Use {@link #PULL_FROM_START} from now on.
         */
        @Deprecated
        public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START;
        /**
         * @deprecated Use {@link #PULL_FROM_END} from now on.
         */
        @Deprecated
        public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END;

        /**
         * Maps an int to a specific mode. This is needed when saving state, or
         * inflating the view from XML where the mode is given through a attr
         * int.
         * 
         * @param modeInt - int to map a Mode to
         * @return Mode that modeInt maps to, or PULL_FROM_START by default.
         */
        static Mode mapIntToValue(final int modeInt)
        {
            for (Mode value : Mode.values())
            {
                if (modeInt == value.getIntValue())
                {
                    return value;
                }
            }
            // If not, return default
            return getDefault();
        }

        static Mode getDefault()
        {
            return PULL_FROM_START;
        }
        private int mIntValue;

        // The modeInt values need to match those from attrs.xml
        Mode(int modeInt)
        {
            mIntValue = modeInt;
        }

        /**
         * @return true if the mode permits Pull-to-Refresh
         */
        boolean permitsPullToRefresh()
        {
            return !(this == DISABLED || this == MANUAL_REFRESH_ONLY);
        }

        /**
         * @return true if this mode wants the Loading Layout Header to be shown
         */
        public boolean showHeaderLoadingLayout()
        {
            return this == PULL_FROM_START || this == BOTH;
        }

        /**
         * @return true if this mode wants the Loading Layout Footer to be shown
         */
        public boolean showFooterLoadingLayout()
        {
            return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
        }

        int getIntValue()
        {
            return mIntValue;
        }
    }

    // ===========================================================
    // Inner, Anonymous Classes, and Enumerations
    // ===========================================================
    /**
     * Simple Listener that allows you to be notified when the user has scrolled
     * to the end of the AdapterView. See (
     * {@link PullToRefreshAdapterViewBase#setOnLastItemVisibleListener}.
     * 
     * @author Chris Banes
     */
    public static interface OnLastItemVisibleListener
    {
        /**
         * Called when the user has scrolled to the end of the list
         */
        public void onLastItemVisible();
    }

    /**
     * Listener that allows you to be notified when the user has started or
     * finished a touch event. Useful when you want to append extra UI events
     * (such as sounds). See (
     * {@link PullToRefreshAdapterViewBase#setOnPullEventListener}.
     * 
     * @author Chris Banes
     */
    public static interface OnPullEventListener<V extends View>
    {
        /**
         * Called when the internal state has been changed, usually by the user
         * pulling.
         * 
         * @param refreshView - View which has had it's state change.
         * @param state - The new state of View.
         * @param direction - One of {@link Mode#PULL_FROM_START} or
         *            {@link Mode#PULL_FROM_END} depending on which direction
         *            the user is pulling. Only useful when <var>state</var> is
         *            {@link State#PULL_TO_REFRESH} or
         *            {@link State#RELEASE_TO_REFRESH}.
         */
        public void onPullEvent(final PullToRefreshBase<V> refreshView , State state , Mode direction);
    }

    /**
     * Simple Listener to listen for any callbacks to Refresh.
     * 
     * @author Chris Banes
     */
    public static interface OnRefreshListener<V extends View>
    {
        /**
         * onRefresh will be called for both a Pull from start, and Pull from
         * end
         */
        public void onRefresh(final PullToRefreshBase<V> refreshView);
    }

    /**
     * An advanced version of the Listener to listen for callbacks to Refresh.
     * This listener is different as it allows you to differentiate between Pull
     * Ups, and Pull Downs.
     * 
     * @author Chris Banes
     */
    public static interface OnRefreshListener2<V extends View>
    {
        // TODO These methods need renaming to START/END rather than DOWN/UP
        /**
         * onPullDownToRefresh will be called only when the user has Pulled from
         * the start, and released.
         */
        public void onPullDownToRefresh(final PullToRefreshBase<V> refreshView);

        /**
         * onPullUpToRefresh will be called only when the user has Pulled from
         * the end, and released.
         */
        public void onPullUpToRefresh(final PullToRefreshBase<V> refreshView);
    }

    public static enum Orientation {
        VERTICAL, HORIZONTAL;
    }

    public static enum State {
        /**
         * When the UI is in a state which means that user is not interacting
         * with the Pull-to-Refresh function.
         */
        RESET(0x0),
        /**
         * When the UI is being pulled by the user, but has not been pulled far
         * enough so that it refreshes when released.
         */
        PULL_TO_REFRESH(0x1),
        /**
         * When the UI is being pulled by the user, and <strong>has</strong>
         * been pulled far enough so that it will refresh when released.
         */
        RELEASE_TO_REFRESH(0x2),
        /**
         * When the UI is currently refreshing, caused by a pull gesture.
         */
        REFRESHING(0x8),
        /**
         * When the UI is currently refreshing, caused by a call to
         * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
         */
        MANUAL_REFRESHING(0x9),
        /**
         * When the UI is currently overscrolling, caused by a fling on the
         * Refreshable View.
         */
        OVERSCROLLING(0x10);
        /**
         * Maps an int to a specific state. This is needed when saving state.
         * 
         * @param stateInt - int to map a State to
         * @return State that stateInt maps to
         */
        static State mapIntToValue(final int stateInt)
        {
            for (State value : State.values())
            {
                if (stateInt == value.getIntValue())
                {
                    return value;
                }
            }
            // If not, return default
            return RESET;
        }
        private int mIntValue;

        State(int intValue)
        {
            mIntValue = intValue;
        }

        int getIntValue()
        {
            return mIntValue;
        }
    }

    final class SmoothScrollRunnable implements Runnable
    {
        private final Interpolator mInterpolator;
        private final int mScrollToY;
        private final int mScrollFromY;
        private final long mDuration;
        private OnSmoothScrollFinishedListener mListener;
        private boolean mContinueRunning = true;
        private long mStartTime = -1;
        private int mCurrentY = -1;

        public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener)
        {
            mScrollFromY = fromY;
            mScrollToY = toY;
            mInterpolator = mScrollAnimationInterpolator;
            mDuration = duration;
            mListener = listener;
        }

        @Override
        public void run()
        {
            /**
             * Only set mStartTime if this is the first time we're starting,
             * else actually calculate the Y delta
             */
            if (mStartTime == -1)
            {
                mStartTime = System.currentTimeMillis();
            }
            else
            {
                /**
                 * We do do all calculations in long to reduce software float
                 * calculations. We use 1000 as it gives us good accuracy and
                 * small rounding errors
                 */
                long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
                normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
                final int deltaY = Math.round((mScrollFromY - mScrollToY) * mInterpolator.getInterpolation(normalizedTime / 1000f));
                mCurrentY = mScrollFromY - deltaY;
                setHeaderScroll(mCurrentY);
            }
            // If we're not at the target Y, keep going...
            if (mContinueRunning && mScrollToY != mCurrentY)
            {
                ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
            }
            else
            {
                if (null != mListener)
                {
                    mListener.onSmoothScrollFinished();
                }
            }
        }

        public void stop()
        {
            mContinueRunning = false;
            removeCallbacks(this);
        }
    }

    static interface OnSmoothScrollFinishedListener
    {
        void onSmoothScrollFinished();
    }
}
